#Logtrack-ng - Module for comunication with DB
#Copyright (C) 2007 Jan Kucera
#
#This program is free software; you can redistribute it and/or
#modify it under the terms of the GNU General Public License
#as published by the Free Software Foundation; either version 2
#of the License, or (at your option) any later version.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

=head1 NAME

Logtrack::DBI

=head1 DESCRIPTION

Collection of frequently used functions with database.


=head2 FUNCTIONS
=over 4

=item insert_incident
=item insert_service
=item insert_event
=item insert_report
=item delete_incident
=item erase_old
=item check_connection

=cut

package Logtrack::DBI;

use strict;
use Exporter;
use DBI;
use Exception qw(:all);
use IO::File;
use Logtrack::Validator;
use Logtrack::Debug;
use Data::Dumper;

use constant TRUE => 1;
use constant FALSE => 0;

sub new {
	my ($self,$data) = @_;
	
	my $db = {};
	my $class = ref $self || $self;
	$db->{data} = $data;
	bless ($db, $class);
	
	if($db->{data}{logfile} eq '') {
		print STDERR 'Logfile is\'n defined!'."\n";
		exit(3);
	}
	
	my $debug = Logtrack::Debug->new();
	my $fd = new IO::File "$db->{data}{logfile}",O_WRONLY|O_APPEND|O_CREAT;
	$debug->set_file_descriptor($fd);
	$db->{debug} = $debug;
	$db->{debug}->debug('Building module DBI..',3);	
	$db->open_db();	
	$db->{debug}->println(' complete!',3);
	
	return $db;
}

sub errstr {
	my ($self,$message) = @_;
	$self->{debug}->errstr($message);
}

sub errstrln {
	my ($self,$message) = @_;
	$self->{debug}->errstrln($message);
}

sub log_in {
	my ($self,$message) = @_;
	$self->{debug}->log_in($message);
}

sub debug {
	my ($self,$message,$level) = @_;
	$self->{debug}->debug($message,$level);
}

sub debugln {
	my ($self,$message,$level) = @_;
	$self->{debug}->debugln($message,$level);
}


=item 
Functions calls DB procedure for inserting or updating record in table 
INCIDENTS. If it succes, function returns record's id otherwise undef value.

Funkce vola proceduru v postreSQL pro vlozeni (pokud incident existuje, tak
aktualizuje pouze jeho pocty) incidentu funkce, pokud uspeje
je vraco id vlozeneho incidentu, jinak vraci nedefinovanou hodnotu.
=cut
sub insert_incident {
	my ($self,$id_event,$domain,$domain_key) = @_;
	my $ref_db = $self->{db};
	my $result = $$ref_db->selectrow_hashref('SELECT insert_incident(?,?,?)',
		{},$id_event,$domain,$domain_key);

	if(not defined $result) {
		$self->errstrln('Fatal error during inserting incidents');
		$self->errstrln($$ref_db->errstr);
		$self->check_connection();
		my $error = new Exception('DBI');
        $error->raise();
		return undef;
	}
	
	$$ref_db->commit;
	return $result->{insert_incident};
}



=item
Function inserts or updates record to table SERVICES according to arguments.

Funkce vklada (aktualizuje) zaznam do tabulky SERVICES, podle toho, zda dostane
1 nebo 2 argumenty pak nastavy dany zaznam.
=cut

sub insert_service {
	my ($self,$service_key,$id_event) = @_;
	if(not defined $id_event) {
		if(exists $self->{services}->{$service_key}) {
			return TRUE if($self->{services}->{$service_key} eq '');
		}

		my ($ok,$ref_db);
		$ref_db = $self->{db};
		
		$ok = $$ref_db->do('SELECT insert_service(?)',{},$service_key);
		
		if($ok) {
			$self->{services}->{$service_key} = '';
			$$ref_db->commit;
			return TRUE;
		}
		else {
			$self->errstr('Fatal error during inserting service'."\n");
			$self->errstr($$ref_db->errstr);
			my $error = new Exception('DBI');
			$error->raise();
			return FALSE;
		}
	}
	else {
		if(exists $self->{services}->{$service_key}) {
			return TRUE if($self->{services}->{$service_key} eq $id_event);
		}

		my ($ok,$ref_db);
		$ref_db = $self->{db};
		
		$ok = $$ref_db->do('SELECT insert_service(?,?)',
				{},$service_key,$id_event);
		
		if($ok) {
			$self->{services}->{$service_key} = $id_event;
			$$ref_db->commit;
			return TRUE;
		}
		else {
			$self->errstr('Fatal error during inserting service'."\n");
			$self->errstr($$ref_db->errstr);
			my $error = new Exception('DBI');
			$error->raise();
			return FALSE;
		}
	}
}

=item
Function for inserting record to table EVENTS, function returns ids.

Funkce vlozi udalost do databaze a vraci jeji id
=cut

sub insert_event {
	my ($self,$source,$ref_orig_msg,$ref_xml) = @_;
	my ($result,$ref_db);
	$ref_db = $self->{db};
	chomp($$ref_orig_msg);
	$result = $$ref_db->selectrow_hashref('SELECT insert_event(?,?,?)',
		{},$source,$$ref_orig_msg,$$ref_xml
	);
	
	if(not defined $result) {
		$self->errstr('Fatal error during inserting event'."\n");
		$self->debug($$ref_db->errstr);
		$self->errstr($$ref_db->errstr);
		my $error = new Exception('DBI');
		$error->raise();
		return undef;
	}

	$$ref_db->commit;
	return $result->{insert_event};
}

=item

Function for inserting reports - month history of records about incidents.
It checks only timestamp argument. If it's undefined value is set to 
DEFAULT and DB inserts actual date.

Funkce pro vkladani zprav (reports) o incidentech za posledni mesic. Funkce
pouze kontroluje, zda je definovan argument timestamp a pokud ne, jen nastaven
na DEFAULT, aby do nej databaze mohla vlozit aktualni cas.

=cut

sub insert_report {
	my ($self,$src,$dst,$login) = @_;
	my ($result,$ref_db);
	$ref_db = $self->{db};
	$result = $$ref_db->selectrow_hashref('SELECT insert_report(?,?,?)',{},
			$src,$dst,$login);
	if(not defined $result) {
		$self->errstr('Fatal error during inserting report'."\n");
		$self->errstr($$ref_db->errstr);
		my $error = new Exception('DBI');
		$error->raise();
		return undef;
	}
	
	$$ref_db->commit;
}

=item
Function erase multiple incidents (or stand-alone).
Basic ID is stored in $id_incident a erasing donw through 
events_incidents and dependencies between id_incident and other incidents.
stored int @id_change. This means, that are erased only incidents that has 
connection with id_incident.

Funkce provede smazani incidentu, pokud je mimo id incidentu specikovano i pole
s dalsimi id. Provede se mazani z events_incidents na bazi zavislosti mezi 
id_incident a zaznamy uvedenych id_change. Tj, provede vymazani incidentu, 
ktere spolu souvisi a sdili spolu zaznamy v udalostech
=cut

sub delete_incident($$@) {
	my ($self,$id_incident,@id_change) = @_;
	my ($ok,$ref_db);
	$ref_db = $self->{db};
	$self->debugln('Executing delete_incident',3);
	
	if(@id_change) {
		my $index = @id_change - 1;
		my $query = $$ref_db->prepare('SELECT delete_incident(?::BIGINT,
									ARRAY[?' .',?' x $index .']::BIGINT[])');
		$ok = $query->execute($id_incident,@id_change);
		$query->finish();
	}
	else {
		$ok = $$ref_db->do('SELECT delete_incident(?)',{},$id_incident);
	}
	
	if($ok) {
		$$ref_db->commit;
		return TRUE;
	}
	else {
		$self->errstr('Fatal error during deleting incident'."\n");
		$self->errstr($ref_db->errstr);
		my $error = new Exception('DBI');
		$error->raise();
		return FALSE;
	}
}

=item
Function trigger SQL functions for erasing old records. First it triggers 
erasing of old events - this decreasing incidents. Each report is related to 
some log source a it has its own time of last erase. Second is update time of 
last erase in table SOURCES. Last action is erasing old reports - this function
erase reports which contains incidents older than month.

Funkce vola SQL procedury na mazani zaznamu. Nejprve provede vymaz udalosti 
starsich vic jak cas urceny pro konkretni zdroj logu. Nasledne je cas posledni
zmeny ulozen do tabulky SOURCES. Nakonec je provedeno smazani zaznamu z REPORTS
jejichz vsechny incidenty jsou starsi jak mesic.
=cut

sub erase_old {
	my ($self,$id,$rotate_time) = @_;
	my ($ref_db,$ok);
	$ref_db = $self->{db};
	
	$ok = $$ref_db->do('SELECT erase_old_events(?,?)',
		{},$id,$rotate_time);
	$ok *= $$ref_db->do(
		'UPDATE sources SET last_erase = now() WHERE id = ?',{},$id);
	$ok *= $$ref_db->do(
		'SELECT erase_old_reports(?)',{},'1 month');
	
	if($ok) {
		$self->{data}{logs}->[$id]->{last_erase} = time;
		$$ref_db->commit;
	}
	else {
		$self->errstr('Fatal error during erasing old records'."\n");
		$self->debug($$ref_db->errstr);
		$self->errstr($$ref_db->errstr);
		$self->check_connection();
		$$ref_db->rollback;
	}
}

sub get_db {
	my ($self) = @_;
	if(defined $self->{db}) {
		return $self->open_db();
	}
	else {
		return $self->open_db();
	}
}

=item
Function tries connection with DB - if it fails, it waits for 2 seconds a try
it again.

Funkce nejprve zkusi, zda spojeni s databazi je funkcni a pokud ne,
pocka 2 vteriny a pokusi se obnovit spojeni - pokud neuspeje, program konci.
=cut

sub check_connection {
	my ($self) = @_;
	my $ref_db = $self->{db};
	if(not $$ref_db->ping) {
		return FALSE;
	}
	return TRUE;
}


=item
Function opens connection with DB and returns actual DB structure
=cut

=item
Funkce otevre spojeni s databazi a vraci ukazatel na danou db strukturu
=cut

sub open_db {
	my ($self) = @_;
	if(defined $self->{db} && $self->check_connection) {
		return $self->{db};
	}

	my $db;
	if(not $db = DBI->connect('dbi:Pg:dbname=logtrack',
								'postgres', '', {AutoCommit => 0})) {
		$self->errstr('Can\'t connect to DB' ."\n");
		exit(1);
	}
	$self->{db} = \$db;
	return \$db;

}

1;